package yscmd

import (
	"context"
	"gitee.com/kmyss/gf-ex/ysbuild"
	"gitee.com/kmyss/gf-ex/yslog"
	"github.com/gogf/gf/v2/container/garray"
	"github.com/gogf/gf/v2/container/gset"
	"github.com/gogf/gf/v2/frame/g"
	"github.com/gogf/gf/v2/os/gcmd"
	"github.com/gogf/gf/v2/os/genv"
	"github.com/gogf/gf/v2/os/gfile"
	"github.com/gogf/gf/v2/text/gstr"
	"github.com/gogf/gf/v2/util/gconv"
	"runtime"
	"strings"
)

const INSTALL = "install"

type CmdInstallBuilder struct {
	CommandBuilderCore
}

func (b *CmdInstallBuilder) SetFlag(IFlagBuilder, ...string) bool {
	return false
}

func (b *CmdInstallBuilder) Init(ctx context.Context) {
	b.CommandBuilderCore.Init(ctx)
	b.C.Run = b.run
	b.C.Use = INSTALL
	b.C.Short = "将程序安装到指定文件夹"

	SetFlag(b, &FlagInstallPathBuilder{})
}

func (b *CmdInstallBuilder) SetChildCommand(ICommandBuilder) bool {
	return false
}

// installFolderPath contains installFolderPath-related data,
// such as path, writable, binaryFilePath, and installed.
type installFolderPath struct {
	path           string
	writable       bool
	binaryFilePath string
	installed      bool
}

func (b *CmdInstallBuilder) run(c *Command) {
	yslog.Debug("CmdVersionBuilder Run")

	//指定路径时安装到指定路径
	installPath := FlagInstallPath(c.ctx)
	if !g.IsEmpty(installPath.Get()) {
		err := gfile.CopyFile(gfile.SelfPath(), gfile.Join(installPath.Get(), ysbuild.APPName+gfile.Ext(gfile.SelfPath())))
		if err != nil {
			yslog.Printf("安装到 '%s' 失败: %v", installPath.Get(), err)
			yslog.Printf("您需要手动将程序拷贝到: %s", installPath.Get())
			CmdWait(c.ctx)
		} else {
			yslog.Printf("安装成功: %s", installPath.Get())
		}
		return
	}

	b.installAuto()
}

func (b *CmdInstallBuilder) installAuto() {
	// Ask where to install.
	paths := getInstallPathsData()
	if len(paths) <= 0 {
		yslog.Printf("没有找到可以安装的目录，您需要手动将程序拷贝到 path 目录.")
		return
	}
	yslog.Printf("我们通过 $PATH 在您电脑里找到如下路径 ")
	yslog.Printf("  %2s | %4s | %5s | %s", "序号", "是否可写", "是否已安装", "路径")

	// Print all paths status and determine the default selectedID value.
	var (
		selectedID = -1
		pathSet    = gset.NewStrSet() // Used for repeated items filtering.
	)
	for id, aPath := range paths {
		if !pathSet.AddIfNotExist(aPath.path) {
			continue
		}
		yslog.Printf("  %4d | %8t | %10t | %s", id, aPath.writable, aPath.installed, aPath.path)
		if selectedID == -1 {
			// Use the previously installed path as the most priority choice.
			if aPath.installed {
				selectedID = id
			}
		}
	}
	// If there's no previously installed path, use the first writable path.
	if selectedID == -1 {
		// Order by choosing priority.
		commonPaths := garray.NewStrArrayFrom(g.SliceStr{
			`/usr/local/bin`,
			`/usr/bin`,
			`/usr/sbin`,
			`C:\Windows`,
			`C:\Windows\system32`,
			`C:\Go\bin`,
			`C:\Program Files`,
			`C:\Program Files (x86)`,
		})
		// Check the common installation directories.
		commonPaths.Iterator(func(k int, v string) bool {
			for id, aPath := range paths {
				if strings.EqualFold(aPath.path, v) {
					selectedID = id
					return false
				}
			}
			return true
		})
		if selectedID == -1 {
			selectedID = 0
		}
	}

	// GetSelectId
	input := gcmd.Scanf("请选择一个序号 [默认： %d]: ", selectedID)
	if input != "" {
		selectedID = gconv.Int(input)
	}

	// Check if out of range.
	if selectedID >= len(paths) || selectedID < 0 {
		yslog.Printf("选择的序号有误： %d", selectedID)
		return
	}

	// Get selected destination path.
	dstPath := paths[selectedID]

	// Install the new binary.
	err := gfile.CopyFile(gfile.SelfPath(), dstPath.binaryFilePath)
	if err != nil {
		yslog.Printf("安装到 '%s' 失败: %v", dstPath.path, err)
		yslog.Printf("您需要手动将程序拷贝到: %s", dstPath.path)
	} else {
		yslog.Printf("安装成功: %s", dstPath.path)
	}

	// Uninstall the old binary.
	for _, aPath := range paths {
		// Do not delete myself.
		if aPath.binaryFilePath != "" &&
			aPath.binaryFilePath != dstPath.binaryFilePath &&
			gfile.SelfPath() != aPath.binaryFilePath {
			_ = gfile.Remove(aPath.binaryFilePath)
		}
	}
}

// IsInstalled returns whether the binary is installed.
func IsInstalled() bool {
	paths := getInstallPathsData()
	for _, aPath := range paths {
		if aPath.installed {
			return true
		}
	}
	return false
}

// GetInstallPathsData returns the installation paths data for the binary.
func getInstallPathsData() []installFolderPath {
	var folderPaths []installFolderPath
	// Pre generate binaryFileName.
	binaryFileName := ysbuild.APPName + gfile.Ext(gfile.SelfPath())
	switch runtime.GOOS {
	case "darwin":
		darwinInstallationCheckPaths := []string{"/usr/local/bin"}
		for _, v := range darwinInstallationCheckPaths {
			folderPaths = checkPathAndAppendToInstallFolderPath(
				folderPaths, v, binaryFileName,
			)
		}
		fallthrough

	default:
		// $GOPATH/bin
		gopath := gfile.Join(runtime.GOROOT(), "bin")
		folderPaths = checkPathAndAppendToInstallFolderPath(
			folderPaths, gopath, binaryFileName,
		)
		// Search and find the writable directory path.
		envPath := genv.Get("PATH", genv.Get("Path")).String()
		if gstr.Contains(envPath, ";") {
			for _, v := range gstr.SplitAndTrim(envPath, ";") {
				folderPaths = checkPathAndAppendToInstallFolderPath(
					folderPaths, v, binaryFileName,
				)
			}
		} else if gstr.Contains(envPath, ":") {
			for _, v := range gstr.SplitAndTrim(envPath, ":") {
				folderPaths = checkPathAndAppendToInstallFolderPath(
					folderPaths, v, binaryFileName,
				)
			}
		} else if envPath != "" {
			folderPaths = checkPathAndAppendToInstallFolderPath(
				folderPaths, envPath, binaryFileName,
			)
		} else {
			folderPaths = checkPathAndAppendToInstallFolderPath(
				folderPaths, "/usr/local/bin", binaryFileName,
			)
		}
	}
	return folderPaths
}

// checkPathAndAppendToInstallFolderPath checks if `path` is writable and already installed.
// It adds the `path` to `folderPaths` if it is writable or already installed, or else it ignores the `path`.
func checkPathAndAppendToInstallFolderPath(folderPaths []installFolderPath, path string, binaryFileName string) []installFolderPath {
	var (
		binaryFilePath = gfile.Join(path, binaryFileName)
		writable       = gfile.IsWritable(path)
		installed      = isInstalled(binaryFilePath)
	)
	if !writable && !installed {
		return folderPaths
	}
	return append(
		folderPaths,
		installFolderPath{
			path:           path,
			writable:       writable,
			binaryFilePath: binaryFilePath,
			installed:      installed,
		})
}

// Check if this gf binary path exists.
func isInstalled(path string) bool {
	return gfile.Exists(path)
}
